/*
 * Copyright (c) 2008 Cameron Zemek
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to
 * deal in the Software without restriction, including without limitation the
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
 * sell copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
 * IN THE SOFTWARE.
 */
package net.zeminvaders.lang;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;

import net.zeminvaders.lang.ast.FunctionNode;
import net.zeminvaders.lang.ast.RootNode;
import net.zeminvaders.lang.runtime.ArrayPushFunction;
import net.zeminvaders.lang.runtime.Function;
import net.zeminvaders.lang.runtime.LenFunction;
import net.zeminvaders.lang.runtime.PrintFunction;
import net.zeminvaders.lang.runtime.PrintLineFunction;
import net.zeminvaders.lang.runtime.UserFunction;
import net.zeminvaders.lang.runtime.ZemObject;

/**
 * Interpreter directly evaluates the abstract syntax tree generated by the
 * Parser. It checks the semantics as it process the tree.
 *
 * @author <a href="mailto:grom@zeminvaders.net">Cameron Zemek</a>
 */
public class Interpreter {
    /**
     * Tracks the variables using global scope as per global keyword
     */
    private Set<String> globalImports = new HashSet<String>();

    private SymbolTable globals = new SymbolTable();
    private SymbolTable symbolTable;
    private Map<FunctionNode, Collection<String>> upvalCache = new WeakHashMap<FunctionNode, Collection<String>>();

    /**
     * Setup interpreter with empty symbol table
     * and register built-in functions.
     */
    public Interpreter() {
        symbolTable = globals;
        // Register built-in functions
        symbolTable.set("print", new PrintFunction());
        symbolTable.set("println", new PrintLineFunction());
        symbolTable.set("len", new LenFunction());
        symbolTable.set("array_push", new ArrayPushFunction());
    }

    /**
     * Setup symbol table for function
     */
    public SymbolTable createSymbolTable(FunctionNode function) {
    	if (symbolTable == globals) {
    		// Top level functions don't have any outer function to capture
    		// variables from
    		return new SymbolTable(globals, symbolTable);
    	}
        Collection<String> upvals = upvalCache.get(function);
        if (upvals == null) {
        	ScopeInfo scope = new ScopeInfo(globalImports, symbolTable.getNames());
        	function.resolveScope(scope);
        	upvals = scope.getUpvals();
        	upvalCache.put(function, upvals);
        }
        return new SymbolTable(globals, symbolTable, upvals);
    }

    /**
     * Import global variable into current scope
     */
    public void importGlobal(String name, SourcePosition pos) {
        globalImports.add(name);
    }

    /**
     * Get the current value of a variable.
     *
     * @param name Variable name
     * @param pos  Source position of where the variable is being requested
     * @return The value of the variable
     */
    public ZemObject getVariable(String name, SourcePosition pos) {
        if (globalImports.contains(name)) {
            return globals.get(name, pos);
        }
        return symbolTable.get(name, pos);
    }

    /**
     * Set the value of a variable
     *
     * @param name  Variable name
     * @param value New value for the variable
     */
    public void setVariable(String name, ZemObject value) {
        if (globalImports.contains(name)) {
        	globals.set(name, value);
        } else {
        	symbolTable.set(name, value);
        }
    }

    /**
     * Call a function.
     *
     * @param function The function to call
     * @param args List of arguments to pass to function
     * @param pos  Position in source code of the function call
     * @param functionName Name of the function. Null is passed for anonymous functions.
     * @return Return value from function
     */
    public ZemObject callFunction(Function function, List<ZemObject> args, SourcePosition pos, String functionName) {
    	// Save the symbolTable
    	SymbolTable savedSymbolTable = symbolTable;
        Set<String> savedGlobalImports = globalImports;
        // Setup symbolTable for function
        if (function instanceof UserFunction) {
            symbolTable = ((UserFunction)function).getSymbolTable();
        }
        symbolTable = new SymbolTable(globals, symbolTable);
        globalImports = new HashSet<String>(globalImports);
        int noMissingArgs = 0;
        int noRequiredArgs = 0;
        for (int paramIndex = 0;
                paramIndex < function.getParameterCount(); paramIndex++) {
            String parameterName = function.getParameterName(paramIndex);
            ZemObject value = function.getDefaultValue(paramIndex);
            if (value == null) {
                noRequiredArgs++;
            }
            if (paramIndex < args.size()) {
                // Value provided in function call overrides the default value
                value = args.get(paramIndex);
            }
            if (value == null) {
                noMissingArgs++;
            }
            setVariable(parameterName, value);
        }
        if (noMissingArgs > 0) {
            throw new TooFewArgumentsException(functionName, noRequiredArgs,
                    args.size(), pos);
        }
        ZemObject ret = function.eval(this, pos);
        // Restore symbolTable
        symbolTable = savedSymbolTable;
        globalImports = savedGlobalImports;
        return ret;
    }

    /**
     * Evaluate script
     *
     * @param script The script to evaluate
     * @return The exit status
     * @throws IOException
     */
    public ZemObject eval(String script) throws IOException {
        return eval(new StringReader(script));
    }

    /**
     * Evaluate script
     *
     * @param file The file that contains the script
     * @return The exit status
     * @throws IOException
     */
    public ZemObject eval(File file) throws IOException {
        return eval(new BufferedReader(new FileReader(file)));
    }

    /**
     * Evaluate script
     *
     * @param reader The reader that contains the script
     * @return The exit status
     * @throws IOException
     */
    public ZemObject eval(Reader reader) throws IOException {
        Lexer lexer = new Lexer(reader);
        Parser parser = new Parser(lexer);
        RootNode program = parser.program();
        return program.eval(this);
    }
}
